#!/usr/bin/env python3
# Licensed under GPLv3
#
# Simple script to query the management interface of a running n2n edge node

import argparse
import socket
import json
import collections


class JsonUDP():
    """encapsulate communication with the edge"""

    def __init__(self, port):
        self.address = "127.0.0.1"
        self.port = port
        self.tag = 0
        self.key = None
        self.debug = False
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.settimeout(1)

    def _next_tag(self):
        tagstr = str(self.tag)
        self.tag = (self.tag + 1) % 1000
        return tagstr

    def _cmdstr(self, msgtype, cmdline):
        """Create the full command string to send"""
        tagstr = self._next_tag()

        options = [tagstr]
        if self.key is not None:
            options += ['1']  # Flags set for auth key field
            options += [self.key]
        optionsstr = ':'.join(options)

        return tagstr, ' '.join((msgtype, optionsstr, cmdline))

    def _rx(self, tagstr):
        """Wait for rx packets"""

        seen_begin = False
        while not seen_begin:
            # TODO: there are no timeouts with any of the recv calls
            data, _ = self.sock.recvfrom(1024)
            data = json.loads(data.decode('utf8'))

            # TODO: We assume the first packet we get will be tagged for us
            assert(data['_tag'] == tagstr)

            if data['_type'] == 'error':
                raise ValueError('Error: {}'.format(data['error']))

            if data['_type'] == 'replacing':
                # a signal that we have evicted an earlier subscribe
                continue

            if data['_type'] == 'subscribe':
                return True

            if data['_type'] == 'begin':
                seen_begin = True

                # Ideally, we would confirm that this is our "begin", but that
                # would need the cmd passed into this method, and that would
                # probably require parsing the cmdline passed to us :-(
                # assert(data['cmd'] == cmd)

                continue

            raise ValueError('Unknown data type {} from '
                             'edge'.format(data['_type']))

        result = list()
        error = None

        while True:
            data, _ = self.sock.recvfrom(1024)
            data = json.loads(data.decode('utf8'))

            if data['_tag'] != tagstr:
                # this packet is not for us, ignore it
                continue

            if data['_type'] == 'error':
                # we still expect an end packet, so save the error
                error = ValueError('Error: {}'.format(data['error']))
                continue

            if data['_type'] == 'end':
                if error:
                    raise error
                return result

            if data['_type'] != 'row':
                raise ValueError('Unknown data type {} from '
                                 'edge'.format(data['_type']))

            # remove our boring metadata
            del data['_tag']
            del data['_type']

            if self.debug:
                print(data)

            result.append(data)

    def _call(self, msgtype, cmdline):
        """Perform a rpc call"""
        tagstr, msgstr = self._cmdstr(msgtype, cmdline)
        self.sock.sendto(msgstr.encode('utf8'), (self.address, self.port))
        return self._rx(tagstr)

    def read(self, cmdline):
        return self._call('r', cmdline)

    def write(self, cmdline):
        return self._call('w', cmdline)

    def sub(self, cmdline):
        return self._call('s', cmdline)

    def readevent(self):
        self.sock.settimeout(3600)

        data, _ = self.sock.recvfrom(1024)
        data = json.loads(data.decode('utf8'))
        # assert(data['_tag'] == tagstr)
        assert(data['_type'] == 'event')

        del data['_tag']
        del data['_type']
        return data


def str_table(rows, columns, orderby):
    """Given an array of dicts, do a simple table print"""
    result = list()
    widths = collections.defaultdict(lambda: 0)

    if len(rows) == 0:
        # No data to show, be sure not to truncate the column headings
        for col in columns:
            widths[col] = len(col)
    else:
        for row in rows:
            for col in columns:
                if col in row:
                    widths[col] = max(widths[col], len(str(row[col])))

    for col in columns:
        if widths[col] == 0:
            widths[col] = 1
        result += "{:{}.{}} ".format(col, widths[col], widths[col])
    result += "\n"

    if orderby is not None:
        rows = sorted(rows, key=lambda row: row.get(orderby, 0))

    for row in rows:
        for col in columns:
            if col in row:
                data = row[col]
            else:
                data = ''
            result += "{:{}} ".format(data, widths[col])
        result += "\n"

    return ''.join(result)


def subcmd_show_supernodes(rpc, args):
    rows = rpc.read('supernodes')
    columns = [
        'version',
        'current',
        'macaddr',
        'sockaddr',
        'uptime',
    ]

    return str_table(rows, columns, args.orderby)


def subcmd_show_edges(rpc, args):
    rows = rpc.read('edges')
    columns = [
        'mode',
        'ip4addr',
        'macaddr',
        'sockaddr',
        'desc',
    ]

    return str_table(rows, columns, args.orderby)


def subcmd_show_help(rpc, args):
    result = 'Commands with pretty-printed output:\n\n'
    for name, cmd in subcmds.items():
        result += "{:12} {}\n".format(name, cmd['help'])

    result += "\n"
    result += "Possble remote commands:\n"
    result += "(those without a pretty-printer will pass-through)\n\n"
    rows = rpc.read('help')
    for row in rows:
        result += "{:12} {}\n".format(row['cmd'], row['help'])
    return result


subcmds = {
    'help': {
        'func': subcmd_show_help,
        'help': 'Show available commands',
    },
    'supernodes': {
        'func': subcmd_show_supernodes,
        'help': 'Show the list of supernodes',
    },
    'edges': {
        'func': subcmd_show_edges,
        'help': 'Show the list of edges/peers',
    },
}


def subcmd_default(rpc, args):
    """Just pass command through to edge"""
    cmdline = ' '.join([args.cmd] + args.args)
    if args.write:
        rows = rpc.write(cmdline)
    elif args.read:
        rows = rpc.read(cmdline)
    elif args.sub:
        if not rpc.sub(cmdline):
            raise ValueError('Could not subscribe')
        while True:
            event = rpc.readevent()
            # FIXME: violates layering..
            print(json.dumps(event, sort_keys=True, indent=4))
    else:
        raise ValueError('Unknown request type')
    return json.dumps(rows, sort_keys=True, indent=4)


def main():
    ap = argparse.ArgumentParser(
            description='Query the running local n2n edge')
    ap.add_argument('-t', '--mgmtport', action='store', default=5644,
                    help='Management Port (default=5644)', type=int)
    ap.add_argument('-k', '--key', action='store',
                    help='Password for mgmt commands')
    ap.add_argument('-d', '--debug', action='store_true',
                    help='Also show raw internal data')
    ap.add_argument('--raw', action='store_true',
                    help='Force cmd to avoid any pretty printing')
    ap.add_argument('--orderby', action='store',
                    help='Hint to a pretty printer on how to sort')

    group = ap.add_mutually_exclusive_group()
    group.add_argument('--read', action='store_true',
                       help='Make a read request (default)')
    group.add_argument('--write', action='store_true',
                       help='Make a write request (only to non pretty'
                       'printed cmds)')
    group.add_argument('--sub', action='store_true',
                       help='Make a subscribe request')

    ap.add_argument('cmd', action='store',
                    help='Command to run (try "help" for list)')
    ap.add_argument('args', action='store', nargs="*",
                    help='Optional args for the command')

    args = ap.parse_args()

    if not args.read and not args.write and not args.sub:
        args.read = True

    if args.raw or (args.cmd not in subcmds):
        func = subcmd_default
    else:
        func = subcmds[args.cmd]['func']

    rpc = JsonUDP(args.mgmtport)
    rpc.debug = args.debug
    rpc.key = args.key

    try:
        result = func(rpc, args)
    except socket.timeout as e:
        print(e)
        exit(1)

    print(result)


if __name__ == '__main__':
    main()
